package neo.aoi;

import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;

import javax.swing.SpinnerNumberModel;

import artofillusion.Scene;
import artofillusion.math.Vec3;
import artofillusion.object.Object3D;
import artofillusion.object.ObjectInfo;
import artofillusion.ui.ComponentsDialog;
import buoy.widget.BCheckBox;
import buoy.widget.BSpinner;
import buoy.widget.BTextField;
import buoy.widget.Widget;
import buoy.widget.WindowWidget;

/**
 * <p>
 * This class provides the functionality for exporting Art of Illusion models
 * into a occupancy grid format (voxelization). The following steps are followed
 * during a AOI model export:
 * </p>
 * 
 * <ol>
 * <li>Each object in the scene is converted into a triangle mesh with the
 * specified 'tolerance' as specified by the AOI API. Objects which can not be
 * converted to a triangle mesh are ignored.</li>
 * <li>For each triangle, an 3D grid of voxels is built. The value of the
 * voxels which intersect the triangle are set to 1; otherwise the voxels are
 * set to zero.</li>
 * <li>The XYZ coordinates of the 1-valued voxels are exported to the file.
 * Zero-valued voxels are omitted.</li>
 * <li>The file is written in the form of a Matlab matrix, which can be easily
 * imported into Matlab.</li>
 * </ol>
 * 
 * <p>
 * The following is an example of how an export of two objects looks like:
 * <code>
 *  <br>m = [
 *  <br>&nbsp; &nbsp; 1 2 3;
 *  <br>&nbsp; &nbsp; 4 5 6;
 *  <br>&nbsp; &nbsp; 7 8 9
 *  <br>&nbsp; ];
 * </code>
 * </p>
 */
public class VoxelExporter {
	/**
	 * Export the given Scene with the specified filename. All objects in the
	 * Scene which can be converted to a Triangle Mesh are converted with the
	 * specified tolerance. Finally the triangle mesh is divided into voxels of
	 * the size defined by the resolution.
	 * 
	 * @param filename
	 *            The name of the file where the voxelized scene will be saved.
	 * @param scene
	 *            The AOI scene to be exported.
	 * @param tol
	 *            Tolerance used while converting to triangle mesh
	 * @param res
	 *            resolution to be used for digitization.
	 * @param fill
	 *            Indicates whether the voxelization should be used filling the
	 *            inside of the object (CSG method) or just over the polygonal
	 *            faces.
	 * @param listener
	 *            Provides the ExportProgressListener to be used by this method
	 *            to report back the progress. This parameter may be null, if no
	 *            progress information is required.
	 * 
	 * @return An ExportResult object which contains the information of the
	 *         export process: the export status and an information message.
	 * 
	 */
	public static ExportResult doExport(String filename, Scene scene,
			double tol, double res, boolean fill,
			ExportProgressListener listener) {
		try {
			PrintWriter out = new PrintWriter(new FileOutputStream(filename));
			long o_count = 0;
			long o_skipped = 0;
			long vox_count = 0;

			out.printf("%% Convertion date: %1$td-%1$tb-%1$ty\n", new Date());
			out.printf("%% Filled         : %b\n", fill);
			out.printf("%% Resolution     : %f\n", res);
			out.printf("objectList=cell("+scene.getNumObjects()+",1);\n");
			out.println();

			for (int i = 0; i < scene.getNumObjects(); i++) {
				ObjectInfo objinfo = scene.getObject(i);
				Object3D obj3D = objinfo.object;
				
				double coordX= objinfo.coords.getOrigin().x;
				double coordY=objinfo.coords.getOrigin().y;
				double coordZ=objinfo.coords.getOrigin().z;
				
				// Find object coordinates in the grid
				double lowXCoord = coordX - (coordX % res);
				double lowYCoord = coordY - (coordY % res);
				double lowZCoord = coordZ - (coordZ % res);
				double highXCoord = coordX + (res - (coordX % res)) % res;
				double highYCoord = coordY + (res - (coordY % res)) % res;
				double highZCoord = coordZ + (res - (coordZ % res)) % res;

				double xc = lowXCoord;
				double yc = lowYCoord;
				double zc = lowZCoord;
				if (Math.abs((coordX - lowXCoord)) > Math.abs((highXCoord - coordX))) {
					xc = highXCoord;
				} else {
					xc = lowXCoord;
				}
				if (Math.abs((coordY - lowYCoord)) > Math.abs((highYCoord - coordY))) {
					yc = highYCoord;
				} else {
					yc = lowYCoord;
				}
				if (Math.abs((coordZ - lowZCoord)) > Math.abs((highZCoord - coordZ))) {
					zc = highZCoord;
				} else {
					zc = lowZCoord;
				}
				out.printf("%% original coordinates     :x= %f,y= %f,z= %f\n", coordX,coordY,coordZ);
				out.printf("%% low coordinates     :x= %f,y= %f,z= %f\n", lowXCoord,lowYCoord,lowZCoord);
				out.printf("%% high Coordinates     :x= %f,y= %f,z= %f\n", highXCoord,highYCoord,highZCoord);
				out.printf("%% new coordinates     :x= %f,y= %f,z= %f\n", xc,yc,zc);

				int objectCount=i+1;
				
				if (listener != null) {
					listener.alertObject(i + 1, scene.getNumObjects());
				}
				
				if (obj3D.canConvertToTriangleMesh() != Object3D.CANT_CONVERT) {
				
					o_count++;
					Set<Vec3> voxels;
					// Fill object
					if (fill && obj3D.isClosed()) {
						voxels = TotalSceneVoxelizer.getVoxelsWithFillingSet(objinfo.object,
								tol, res,objinfo.coords);
					} else {
						// Get voxels of boundary
						voxels = TotalSceneVoxelizer.getVoxels(obj3D, tol, res,objinfo.coords);
					}

					// Output voxels to file
					String objname = objinfo.name.replaceAll(" ", "_");
					out.println(objname + " = [ ");
					for (Iterator<Vec3> it = voxels.iterator(); it.hasNext();) {
						Vec3 vox = it.next();
						out.printf("    %.4f    %.4f    %.4f", vox.x, vox.y,
								vox.z);
						if (it.hasNext()) {
							out.println(";");
						}
					}
					out.println(" ];");
				
					out.println("objectList{"+objectCount+",1}="+objname+";");
					
					out.println();
					vox_count += voxels.size();

				} else {
					out.println("% Can't convert: " + objinfo.name);
					o_skipped++;
				}
			}
			out.println("voxelSide="+ res+";");
			out.println();
			out.close();

			return new ExportResult(ExportResult.STATUS.OK,
					"File exported successfully!\n" + "  File name: "
							+ filename + "\n" + "  Total objects: " + o_count
							+ "\n" + "  Total voxels : " + vox_count + "\n"
							+ "  Skipped objects: " + o_skipped);
		} catch (Exception e) {
			e.printStackTrace();
			return new ExportResult(ExportResult.STATUS.ERROR, e.getMessage());
		}
	}

	/**
	 * Prompt the user for input of the export parameters. The given
	 * WindowWidget parent is used to display a Dialog window. Once the user has
	 * filled the required fields and clicked OK, the parameters are bundled in
	 * an ExportParameters object which is then returned.
	 * 
	 * @param parent
	 *            The WindowWidget which will be used to display the Dialog
	 *            window.
	 * @return an ExportParameters object containing the parameter values
	 *         entered by the user.
	 */
	public static ExportParameters getExportParameters(WindowWidget parent) {
		return getExportParameters(parent, "Grid (Voxel) Exporter", "default.m");
	}

	/**
	 * Prompt the user for input of the export parameters. The given
	 * WindowWidget parent is used to display a Dialog window. Once the user has
	 * filled the required fields and clicked OK, the parameters are bundled in
	 * an ExportParameters object which is then returned. The title label on top
	 * of the input window and the default file name must also be provided. If
	 * the parameter file is null, then the input window will not display the
	 * file name field.
	 * 
	 * @param parent
	 *            The WindowWidget which will be used to display the Dialog
	 *            window.
	 * @param title
	 *            The title to be displayed on top of the input window.
	 * @param file
	 *            The default name of the file, or null if the file name is not
	 *            required.
	 * @return an ExportParameters object containing the parameter values
	 *         entered by the user.
	 */
	public static ExportParameters getExportParameters(WindowWidget parent,
			String title, String file) {
		BSpinner res_spinner = new BSpinner(new SpinnerNumberModel(1.0, -10.0,
				10.0, 0.01));
		BSpinner tol_spinner = new BSpinner(new SpinnerNumberModel(0.05, -10.0,
				10.0, 0.01));
		BTextField filename = new BTextField(file);
		BCheckBox filling = new BCheckBox("Fill object", false);

		Widget[] widgets;
		String[] labels;

		if (file == null) {
			widgets = new Widget[] { tol_spinner, res_spinner, filling };
			labels = new String[] { "Tolerance", "Resolution", "" };
		} else {
			widgets = new Widget[] { tol_spinner, res_spinner, filename,
					filling };
			labels = new String[] { "Tolerance", "Resolution", "File name:", "" };
		}
		ComponentsDialog dlg = new ComponentsDialog(parent, title, widgets,
				labels);

		return new ExportParameters((Double) res_spinner.getValue(),
				(Double) tol_spinner.getValue(), filename.getText(), filling
						.getState(), dlg.clickedOk());
	}

	/**
	 * This internal class contains the parameters required while exporting.
	 * 
	 * @author Carlos Martinez
	 */
	public static class ExportParameters {
		private double _res; // Export grid resolution

		private double _tolerance; // Triangle mesh tolerance

		private String _filename; // Export file name

		private boolean _fill; // Fill object

		private boolean _go;

		/**
		 * Creates a new ExportParameters object with the given values
		 * 
		 * @param res
		 *            The resolution to be used while exporting. This means that
		 *            each exported voxel will be of the size 'res' relative to
		 *            each unit of the original model size. For example, if a
		 *            1-unit per side cube is exported with resolution 0.1, the
		 *            exported object will have 10 voxels per side.
		 * @param tol
		 *            The tolerance used while converting the AOI objects to
		 *            triangle mesh.
		 * @param fname
		 *            Filename where the objects will be saved.
		 * @param fill
		 *            Whether the object should be filled or should it remain
		 *            empty.
		 * @param ok
		 *            Boolean value indicating if the user clicked the 'OK'
		 *            button.
		 */
		public ExportParameters(double res, double tol, String fname,
				boolean fill, boolean ok) {
			_res = res;
			_tolerance = tol;
			_filename = fname;
			_fill = fill;
			_go = ok;
		}

		/**
		 * Returns the resolution value selected by the user.
		 * 
		 * @return The resolution
		 */
		public double getResolution() {
			return _res;
		}

		/**
		 * Returns the tollerance value selected by the user.
		 * 
		 * @return The tolerance
		 */
		public double getTolerance() {
			return _tolerance;
		}

		/**
		 * The filename indicated by the user.
		 * 
		 * @return The filename
		 */
		public String getFileName() {
			return _filename;
		}

		/**
		 * Whether the object should be filled or should it remain empty.
		 * 
		 * @return True if the object should be filled, false otherwise
		 */
		public boolean fill() {
			return _fill;
		}

		/**
		 * Returns the user's approval.
		 * 
		 * @return true if the user clicked 'OK', false otherwise.
		 */
		public boolean proceed() {
			return _go;
		}
	}

	/**
	 * This class contains the result of exporting a scene along with any
	 * message regarding the export.
	 * 
	 * @author Carlos Martinez
	 */
	public static class ExportResult {

		public enum STATUS {
			OK, ERROR
		};

		private STATUS _status;

		private String _msg;

		/**
		 * Creates a ExportResult object with the indicated status and an
		 * corresponding message.
		 * 
		 * @param status
		 *            The status of this ExportResult
		 * @param message
		 *            A message corresponding to this ExportResult
		 */
		public ExportResult(STATUS status, String message) {
			_status = status;
			_msg = message;
		}

		/**
		 * Returns the status of this ExportResult
		 * 
		 * @return the result status
		 */
		public STATUS getStatus() {
			return _status;
		}

		/**
		 * Returns the corresponding message to this ExportResult
		 * 
		 * @return The message
		 */
		public String getMessage() {
			return _msg;
		}
	}
}
